diff --git a/dd-java-agent/agent-ci-visibility/civisibility-test-fixtures/src/main/java/datadog/trace/civisibility/CiVisibilitySmokeTest.java b/dd-java-agent/agent-ci-visibility/civisibility-test-fixtures/src/main/java/datadog/trace/civisibility/CiVisibilitySmokeTest.java index 5481a87ad47..859f95c917c 100644 --- a/dd-java-agent/agent-ci-visibility/civisibility-test-fixtures/src/main/java/datadog/trace/civisibility/CiVisibilitySmokeTest.java +++ b/dd-java-agent/agent-ci-visibility/civisibility-test-fixtures/src/main/java/datadog/trace/civisibility/CiVisibilitySmokeTest.java @@ -100,7 +100,7 @@ private static Map buildJvmArgMap( protected List buildJvmArguments( String mockBackendIntakeUrl, String serviceName, Map additionalArgs) { - List arguments = new ArrayList<>(Arrays.asList("-Xms256m", "-Xmx256m")); + List arguments = new ArrayList<>(Arrays.asList("-Xms256m", "-Xmx512m")); arguments.add(preventJulPrefsFileLock()); diff --git a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/AbstractGradleTest.java b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/AbstractGradleTest.java index 7ba519bf935..68c2b2832ef 100644 --- a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/AbstractGradleTest.java +++ b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/AbstractGradleTest.java @@ -1,6 +1,7 @@ package datadog.smoketest; import datadog.environment.JavaVirtualMachine; +import datadog.environment.OperatingSystem; import datadog.trace.civisibility.CiVisibilitySmokeTest; import datadog.trace.util.ComparableVersion; import java.io.IOException; @@ -15,8 +16,10 @@ import java.util.Collections; import java.util.Map; import java.util.Properties; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Stream; import org.gradle.internal.impldep.org.apache.commons.io.FileUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assumptions; @@ -35,6 +38,11 @@ public abstract class AbstractGradleTest extends CiVisibilitySmokeTest { private static final ComparableVersion GRADLE_9 = new ComparableVersion("9.0.0"); + // Gradle daemons may keep file handles on their temp directory open for a short while after being + // stopped; retry a few times to give them a chance to release before giving up. + private static final int TEMP_DIR_CLEANUP_RETRIES = 10; + private static final long TEMP_DIR_CLEANUP_RETRY_DELAY_MILLIS = 200; + @TempDir protected Path projectFolder; protected final MockBackend mockBackend = new MockBackend(); @@ -49,6 +57,78 @@ void closeMockBackend() throws Exception { mockBackend.close(); } + /** + * Recursively deletes a directory that a Gradle daemon has been writing into, on a best-effort + * basis. We delete it ourselves (rather than letting JUnit's {@code @TempDir} cleanup do it at + * class teardown) because a daemon may not have released its file handles on {@code + * caches/} by the time the recursive delete runs, which makes the delete fail with a + * {@link java.nio.file.DirectoryNotEmptyException}. + */ + protected static void deleteTempDirectoryQuietly(Path directory) { + if (directory == null) { + return; + } + for (int attempt = 0; + attempt < TEMP_DIR_CLEANUP_RETRIES && Files.exists(directory); + attempt++) { + FileUtils.deleteQuietly(directory.toFile()); + if (!Files.exists(directory)) { + return; + } + try { + Thread.sleep(TEMP_DIR_CLEANUP_RETRY_DELAY_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + if (Files.exists(directory)) { + System.err.println( + "WARNING: could not fully delete temp directory " + + directory + + " after stopping Gradle daemons; leaving it for the OS to reap. " + + "A Gradle daemon likely still holds a file handle on it."); + } + } + + /** Kills the Gradle daemons whose logs live under {@code testKitDir}, on a best-effort basis. */ + protected static void killGradleDaemonsIn(Path testKitDir) { + if (testKitDir == null || !Files.exists(testKitDir)) { + return; + } + boolean windows = OperatingSystem.isWindows(); + try (Stream files = Files.walk(testKitDir)) { + files + .filter(Files::isRegularFile) + .forEach( + file -> { + String name = file.getFileName().toString(); + if (!name.startsWith("daemon-") || !name.endsWith(".out.log")) { + return; + } + String pid = + name.substring("daemon-".length(), name.length() - ".out.log".length()); + if (!pid.matches("\\d+")) { + // skip the UUID fallback Gradle uses when the PID is unavailable + return; + } + ProcessBuilder kill = + windows + ? new ProcessBuilder("taskkill", "/F", "/PID", pid) + : new ProcessBuilder("kill", pid); + try { + kill.redirectErrorStream(true).start().waitFor(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + // best effort — the daemon may already be stopped + } + }); + } catch (Exception e) { + // best effort — failing to enumerate daemon logs must not fail the test run + } + } + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{(.+?)\\}"); protected void givenGradleProjectFiles(String projectFilesSources) throws IOException { diff --git a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java index 31d30a88998..add663ebaa8 100644 --- a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java +++ b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java @@ -29,7 +29,9 @@ import org.gradle.wrapper.Install; import org.gradle.wrapper.PathAssembler; import org.gradle.wrapper.WrapperConfiguration; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.tabletest.junit.TableTest; @@ -43,7 +45,22 @@ class GradleDaemonSmokeTest extends AbstractGradleTest { // Gradle's default timeout is 10s private static final int GRADLE_DISTRIBUTION_NETWORK_TIMEOUT = 30_000; - @TempDir static Path testKitFolder; + // Cleanup is handled manually in stopGradleTestKitDaemons() instead of by JUnit: the TestKit + // daemons may still hold file handles on this directory at class teardown, which would make + // JUnit's recursive delete fail and turn the class into an executionError. + @TempDir(cleanup = CleanupMode.NEVER) + static Path testKitFolder; + + @AfterAll + void stopGradleTestKitDaemons() { + try { + DefaultGradleConnector.close(); + } catch (Exception e) { + System.err.println("Failed to stop Gradle TestKit daemons during cleanup: " + e); + } + killGradleDaemonsIn(testKitFolder); + deleteTempDirectoryQuietly(testKitFolder); + } @TableTest({ "scenario | gradleVersion | projectName | successExpected | expectedTraces | expectedCoverages", diff --git a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleLauncherSmokeTest.java b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleLauncherSmokeTest.java index 822a981ea03..63a0295e0ff 100644 --- a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleLauncherSmokeTest.java +++ b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleLauncherSmokeTest.java @@ -2,11 +2,13 @@ import datadog.communication.util.IOUtils; import datadog.trace.civisibility.utils.ShellCommandExecutor; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.opentest4j.AssertionFailedError; @@ -23,6 +25,7 @@ class GradleLauncherSmokeTest extends AbstractGradleTest { private static final Logger LOGGER = LoggerFactory.getLogger(GradleLauncherSmokeTest.class); private static final int GRADLE_BUILD_TIMEOUT_MILLIS = 90_000; + private static final int GRADLE_STOP_TIMEOUT_MILLIS = 30_000; private static final int GRADLE_WRAPPER_RETRIES = 3; private static final String JAVA_HOME = buildJavaHome(); @@ -73,6 +76,40 @@ void testGradleLauncherInjectsTracerIntoGradleDaemon( cmdLineParams != null ? cmdLineParams : "-Duser.country=VALUE_FROM_GRADLE_PROPERTIES_FILE"); } + /** + * Stops the Gradle build daemon spawned by the launcher after each test. Even though the launcher + * is run with {@code --no-daemon}, Gradle still starts a single-use build daemon that writes into + * {@code $GRADLE_USER_HOME/daemon//}; if that process has not fully released its file + * handles by the time JUnit deletes the shared (static) {@link #gradleUserHome} temp directory at + * class teardown, the recursive delete fails with a {@code DirectoryNotEmptyException}. Stopping + * the daemon here releases those handles ahead of cleanup. + * + *

This runs in {@code @AfterEach} rather than {@code @AfterAll} on purpose: {@link + * #projectFolder} (which holds the {@code gradlew} script used as the working directory) is an + * instance {@code @TempDir}, so JUnit deletes it at the end of each test invocation. By the time + * an {@code @AfterAll} method would run, that working directory no longer exists and the {@code + * --stop} command could not be launched. + */ + @AfterEach + void stopGradleBuildDaemon() { + if (!Files.exists(projectFolder.resolve("gradlew"))) { + // The test was skipped or failed before the wrapper was set up, so no daemon was started. + return; + } + Map env = new HashMap<>(); + env.put("JAVA_HOME", JAVA_HOME); + env.put("GRADLE_USER_HOME", gradleUserHome.toString()); + env.put("GRADLE_OPTS", ""); + ShellCommandExecutor shellCommandExecutor = + new ShellCommandExecutor(projectFolder.toFile(), GRADLE_STOP_TIMEOUT_MILLIS, env); + try { + shellCommandExecutor.executeCommand(IOUtils::readFully, "./gradlew", "--stop"); + } catch (Exception e) { + // Best-effort: a failure here should not fail the test run. + LOGGER.warn("Failed to stop Gradle daemon during cleanup", e); + } + } + private void givenGradleWrapper(String gradleVersion) throws Exception { Map env = new HashMap<>(); env.put("JAVA_HOME", JAVA_HOME);