Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ private static Map<String, String> buildJvmArgMap(

protected List<String> buildJvmArguments(
String mockBackendIntakeUrl, String serviceName, Map<String, String> additionalArgs) {
List<String> arguments = new ArrayList<>(Arrays.asList("-Xms256m", "-Xmx256m"));
List<String> arguments = new ArrayList<>(Arrays.asList("-Xms256m", "-Xmx512m"));

arguments.add(preventJulPrefsFileLock());

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -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/<version>} 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<Path> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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/<version>/}; 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.
*
* <p>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<String, String> 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use an existing project directory for daemon shutdown

Because projectFolder is an instance @TempDir from AbstractGradleTest, JUnit scopes it to each parameterized invocation and cleans it up when that invocation finishes; by the time this @AfterAll runs, the last projectFolder has already been deleted. In that state ShellCommandExecutor starts ./gradlew --stop with a non-existent working directory, the exception is swallowed by the best-effort catch, and the shared static gradleUserHome is still left for JUnit to delete while Gradle daemons may hold files open.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in c36724f

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<String, String> env = new HashMap<>();
env.put("JAVA_HOME", JAVA_HOME);
Expand Down