diff --git a/dd-java-agent/agent-profiling/profiling-controller/build.gradle b/dd-java-agent/agent-profiling/profiling-controller/build.gradle index e255fdf668d..8ab594ad573 100644 --- a/dd-java-agent/agent-profiling/profiling-controller/build.gradle +++ b/dd-java-agent/agent-profiling/profiling-controller/build.gradle @@ -25,6 +25,5 @@ dependencies { testImplementation libs.bundles.junit5 testImplementation libs.guava testImplementation libs.bundles.mockito - testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.0.1' + testImplementation libs.awaitility } - diff --git a/dd-smoke-tests/build.gradle b/dd-smoke-tests/build.gradle index 15e070ea84d..351fd66f060 100644 --- a/dd-smoke-tests/build.gradle +++ b/dd-smoke-tests/build.gradle @@ -10,18 +10,10 @@ dependencies { api project(':utils:test-agent-utils:decoder') compileOnly(libs.junit.jupiter) - compileOnly(libs.bundles.groovy) compileOnly(libs.bundles.spock) -} -tasks.withType(GroovyCompile).configureEach { - configureCompiler( - it, - 8, - JavaVersion.VERSION_1_8, - "CharBuffer#flip() signature needs to be ()Ljava.nio.Buffer (to run on JDK8)." - ) + implementation libs.awaitility } // Disable forbiddenApis for all smoke-test modules diff --git a/dd-smoke-tests/concurrent/java-25/build.gradle b/dd-smoke-tests/concurrent/java-25/build.gradle index 9a9e9c1b858..8732c72b073 100644 --- a/dd-smoke-tests/concurrent/java-25/build.gradle +++ b/dd-smoke-tests/concurrent/java-25/build.gradle @@ -3,38 +3,32 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { id 'application' id 'com.gradleup.shadow' + id 'idea' } apply from: "$rootDir/gradle/java.gradle" testJvmConstraints { - // This smoke test should be limited to Java 25 and above - // But the groovy testing framework cannot run on Java 25 - // Using Java 8 for now and runs a JVM 25 when forking tests process. - // Relying on forked JVM 25 is hardcoded in the test suites (see createProcessBuilder()). - minJavaVersion = JavaVersion.VERSION_1_8 - maxJavaVersion = JavaVersion.VERSION_1_8 - // Only runs on Temurin build as it spawns a Temurin 25 for test process. - excludeJdk = ['IBM8', 'SEMERU8'] + minJavaVersion = JavaVersion.VERSION_25 } description = 'JDK 25 Concurrent Integration Tests' -javaToolchains { - java { - toolchain { - languageVersion = JavaLanguageVersion.of(25) - } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) } } -tasks.named('compileJava', JavaCompile) { - configureCompiler(it, 25, JavaVersion.VERSION_25) - options.compilerArgs.add("--enable-preview") +idea { + module { + jdkName = '25' + } } -tasks.named('compileTestGroovy') { - configureCompiler(it, 8, JavaVersion.VERSION_1_8) +tasks.withType(JavaCompile).configureEach { + configureCompiler(it, 25, JavaVersion.VERSION_25) + options.compilerArgs.add("--enable-preview") } application { @@ -44,6 +38,7 @@ application { dependencies { implementation group: 'io.opentelemetry.instrumentation', name: 'opentelemetry-instrumentation-annotations', version: '2.19.0' testImplementation project(':dd-smoke-tests') + testImplementation libs.bundles.junit5 } tasks.withType(Test).configureEach { diff --git a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/AbstractStructuredConcurrencyTest.groovy b/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/AbstractStructuredConcurrencyTest.groovy deleted file mode 100644 index 72ef109fa0b..00000000000 --- a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/AbstractStructuredConcurrencyTest.groovy +++ /dev/null @@ -1,54 +0,0 @@ -package datadog.smoketest.concurrent - -import datadog.smoketest.AbstractSmokeTest -import datadog.trace.test.agent.decoder.DecodedSpan -import datadog.trace.test.agent.decoder.DecodedTrace - -import java.nio.file.Paths -import java.util.function.Function - -import static java.util.concurrent.TimeUnit.SECONDS - -abstract class AbstractStructuredConcurrencyTest extends AbstractSmokeTest { - protected static final int TIMEOUT_SECS = 10 - protected abstract String testCaseName() - - @Override - ProcessBuilder createProcessBuilder() { - def jarPath = System.getProperty("datadog.smoketest.shadowJar.path") - def command = new ArrayList() - - def javaHome = Paths.get(System.getenv("JAVA_HOME"), "bin", "java").toString() - command.add(javaHome) - command.addAll(defaultJavaProperties) - command.add("--enable-preview") - command.add("-Ddd.trace.otel.enabled=true") - command.addAll(["-jar", jarPath]) - command.add(testCaseName()) - - ProcessBuilder processBuilder = new ProcessBuilder(command) - processBuilder.directory(new File(buildDirectory)) - } - - @Override - Closure decodedTracesCallback() { - return {} // force traces decoding - } - - protected void receivedCorrectTrace() { - assert testedProcess.waitFor(TIMEOUT_SECS, SECONDS) - assert testedProcess.exitValue() == 0 - waitForTrace(defaultPoll, checkTrace()) - assert traceCount.get() == 1 - } - - protected abstract Function checkTrace() - - protected DecodedSpan findRootSpan(DecodedTrace trace, String resource) { - return trace.spans.find { it.resource == resource && it.parentId == 0 } - } - - protected DecodedSpan findChildSpan(DecodedTrace trace, String resource, long parentSpanId) { - return trace.spans.find { it.resource == resource && it.parentId == parentSpanId } - } -} diff --git a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/MultipleTasksTest.groovy b/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/MultipleTasksTest.groovy deleted file mode 100644 index 56f50e358e8..00000000000 --- a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/MultipleTasksTest.groovy +++ /dev/null @@ -1,40 +0,0 @@ -package datadog.smoketest.concurrent - -import datadog.trace.test.agent.decoder.DecodedTrace - -import java.util.function.Function - -/** - * Tests the structured task scope with a multiple tasks. - * Here is the expected task/span structure: - *
- *   parent
- *   |-- child1
- *   |-- child2
- *   \-- child3
- * 
- */ -class MultipleTasksTest extends AbstractStructuredConcurrencyTest { - @Override - protected String testCaseName() { - 'MultipleTasks' - } - - @Override - protected Function checkTrace() { - return { - trace -> - // Look for 'parent' span - def parentSpan = findRootSpan(trace, 'parent') - // Look for 'child1', 'child2', and 'child3' spans - return parentSpan && findChildSpan(trace, 'child1', parentSpan.spanId) - && findChildSpan(trace, 'child2', parentSpan.spanId) - && findChildSpan(trace, 'child3', parentSpan.spanId) - } - } - - def 'test multiple tasks'() { - expect: - receivedCorrectTrace() - } -} diff --git a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/NestedTasksTest.groovy b/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/NestedTasksTest.groovy deleted file mode 100644 index cc5df9c227f..00000000000 --- a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/NestedTasksTest.groovy +++ /dev/null @@ -1,43 +0,0 @@ -package datadog.smoketest.concurrent - -import datadog.trace.test.agent.decoder.DecodedTrace - -import java.util.function.Function - -/** - * Tests the structured task scope with a multiple nested tasks. - * Here is the expected task/span structure: - *
- *   parent
- *   |-- child1
- *   |   |-- great-child1-1
- *   |   \-- great-child1-2
- *   \-- child2
- * 
- */ -class NestedTasksTest extends AbstractStructuredConcurrencyTest { - @Override - protected String testCaseName() { - 'NestedTasks' - } - - @Override - protected Function checkTrace() { - return { - trace -> - // Look for 'parent' span - def parentSpan = findRootSpan(trace, 'parent') - // Look for 'child1' span - def child1Span = parentSpan ? findChildSpan(trace, 'child1', parentSpan.spanId) : null - // Look for 'child2', 'great-child1-1' and 'great-child1-2' spans - return child1Span && findChildSpan(trace, 'child2', parentSpan.spanId) - && findChildSpan(trace, 'great-child1-1', child1Span.spanId) - && findChildSpan(trace, 'great-child1-2', child1Span.spanId) - } - } - - def 'test nested tasks'() { - expect: - receivedCorrectTrace() - } -} diff --git a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/SimpleTest.groovy b/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/SimpleTest.groovy deleted file mode 100644 index b4ba0a4c0b0..00000000000 --- a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/SimpleTest.groovy +++ /dev/null @@ -1,42 +0,0 @@ -package datadog.smoketest.concurrent - -import datadog.trace.test.agent.decoder.DecodedTrace - -import java.util.function.Function - -abstract class SimpleTest extends AbstractStructuredConcurrencyTest { - @Override - protected Function checkTrace() { - return { trace -> - // Look for 'parent' span - def parentSpan = findRootSpan(trace, 'parent') - // Look for 'child' span - return parentSpan && findChildSpan(trace, 'child', parentSpan.spanId) - } - } -} - -class SimpleRunnableTest extends SimpleTest { - @Override - protected String testCaseName() { - 'SimpleRunnableTask' - } - - def 'test simple runnable'() { - expect: - receivedCorrectTrace() - } -} - -class SimpleCallableTest extends SimpleTest { - @Override - protected String testCaseName() { - 'SimpleCallableTask' - } - - def 'test simple callable'() { - expect: - receivedCorrectTrace() - } -} - diff --git a/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/AbstractStructuredConcurrencyTest.java b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/AbstractStructuredConcurrencyTest.java new file mode 100644 index 00000000000..e87f67019fd --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/AbstractStructuredConcurrencyTest.java @@ -0,0 +1,71 @@ +package datadog.smoketest.concurrent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.smoketest.AbstractJavaSmokeTest; +import datadog.trace.test.agent.decoder.DecodedSpan; +import datadog.trace.test.agent.decoder.DecodedTrace; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +abstract class AbstractStructuredConcurrencyTest extends AbstractJavaSmokeTest { + private static final int TIMEOUT_SECONDS = 10; + + protected abstract String testCaseName(); + + protected abstract Predicate checkTrace(); + + @Override + protected ProcessBuilder createProcessBuilder() { + String applicationJar = System.getProperty("datadog.smoketest.shadowJar.path"); + Path javaExecutable = Path.of(System.getenv("JAVA_HOME"), "bin", "java"); + + var processBuilder = new ProcessBuilder(javaExecutable.toString()); + var command = processBuilder.command(); + command.addAll(defaultJavaProperties); + command.addAll( + List.of( + "--enable-preview", + "-Ddd.trace.otel.enabled=true", + "-jar", + applicationJar, + testCaseName())); + + processBuilder.directory(buildDirectory.toFile()); + return processBuilder; + } + + protected void receivedCorrectTrace() throws Exception { + Process process = testedProcess; + assertTrue( + process.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS), + "Instrumented process did not exit within " + TIMEOUT_SECONDS + " seconds"); + assertEquals(0, process.exitValue(), "Instrumented process exited abnormally"); + waitForTrace(checkTrace()); + assertEquals(1, traceCount.get()); + } + + protected Optional findRootSpan(DecodedTrace trace, String resource) { + return trace.getSpans().stream() + .filter(span -> Objects.equals(resource, span.getResource()) && span.getParentId() == 0) + .findFirst(); + } + + protected Optional findChildSpan( + DecodedTrace trace, String resource, long parentSpanId) { + return trace.getSpans().stream() + .filter( + span -> + Objects.equals(resource, span.getResource()) && span.getParentId() == parentSpanId) + .findFirst(); + } + + protected boolean hasChildSpan(DecodedTrace trace, String resource, long parentSpanId) { + return findChildSpan(trace, resource, parentSpanId).isPresent(); + } +} diff --git a/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/MultipleTasksTest.java b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/MultipleTasksTest.java new file mode 100644 index 00000000000..7eccf24c5d8 --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/MultipleTasksTest.java @@ -0,0 +1,41 @@ +package datadog.smoketest.concurrent; + +import datadog.trace.test.agent.decoder.DecodedTrace; +import java.util.function.Predicate; +import org.junit.jupiter.api.Test; + +/** + * Tests the structured task scope with multiple tasks. Here is the expected task/span structure: + * + *
+ *   parent
+ *   |-- child1
+ *   |-- child2
+ *   \-- child3
+ * 
+ */ +class MultipleTasksTest extends AbstractStructuredConcurrencyTest { + @Override + protected String testCaseName() { + return "MultipleTasks"; + } + + @Override + protected Predicate checkTrace() { + // 'parent' with 'child1', 'child2', and 'child3' as direct children + return trace -> + trace.getSpans().size() == 4 + && findRootSpan(trace, "parent") + .filter( + parent -> + hasChildSpan(trace, "child1", parent.getSpanId()) + && hasChildSpan(trace, "child2", parent.getSpanId()) + && hasChildSpan(trace, "child3", parent.getSpanId())) + .isPresent(); + } + + @Test + void testMultipleTasks() throws Exception { + receivedCorrectTrace(); + } +} diff --git a/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/NestedTasksTest.java b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/NestedTasksTest.java new file mode 100644 index 00000000000..fad04954019 --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/NestedTasksTest.java @@ -0,0 +1,48 @@ +package datadog.smoketest.concurrent; + +import datadog.trace.test.agent.decoder.DecodedTrace; +import java.util.function.Predicate; +import org.junit.jupiter.api.Test; + +/** + * Tests the structured task scope with multiple nested tasks. Here is the expected task/span + * structure: + * + *
+ *   parent
+ *   |-- child1
+ *   |   |-- great-child1-1
+ *   |   \-- great-child1-2
+ *   \-- child2
+ * 
+ */ +class NestedTasksTest extends AbstractStructuredConcurrencyTest { + @Override + protected String testCaseName() { + return "NestedTasks"; + } + + @Override + protected Predicate checkTrace() { + // 'parent' -> 'child1' -> {'great-child1-1', 'great-child1-2'}, and 'parent' -> 'child2' + return trace -> + trace.getSpans().size() == 5 + && findRootSpan(trace, "parent") + .filter( + parent -> + findChildSpan(trace, "child1", parent.getSpanId()) + .filter( + child1 -> + hasChildSpan(trace, "child2", parent.getSpanId()) + && hasChildSpan(trace, "great-child1-1", child1.getSpanId()) + && hasChildSpan( + trace, "great-child1-2", child1.getSpanId())) + .isPresent()) + .isPresent(); + } + + @Test + void testNestedTasks() throws Exception { + receivedCorrectTrace(); + } +} diff --git a/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/SimpleCallableTest.java b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/SimpleCallableTest.java new file mode 100644 index 00000000000..81b309a3703 --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/SimpleCallableTest.java @@ -0,0 +1,15 @@ +package datadog.smoketest.concurrent; + +import org.junit.jupiter.api.Test; + +class SimpleCallableTest extends SimpleTest { + @Override + protected String testCaseName() { + return "SimpleCallableTask"; + } + + @Test + void testSimpleCallable() throws Exception { + receivedCorrectTrace(); + } +} diff --git a/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/SimpleRunnableTest.java b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/SimpleRunnableTest.java new file mode 100644 index 00000000000..e09ee9372b9 --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/SimpleRunnableTest.java @@ -0,0 +1,15 @@ +package datadog.smoketest.concurrent; + +import org.junit.jupiter.api.Test; + +class SimpleRunnableTest extends SimpleTest { + @Override + protected String testCaseName() { + return "SimpleRunnableTask"; + } + + @Test + void testSimpleRunnable() throws Exception { + receivedCorrectTrace(); + } +} diff --git a/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/SimpleTest.java b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/SimpleTest.java new file mode 100644 index 00000000000..e78c881ce4c --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/SimpleTest.java @@ -0,0 +1,16 @@ +package datadog.smoketest.concurrent; + +import datadog.trace.test.agent.decoder.DecodedTrace; +import java.util.function.Predicate; + +abstract class SimpleTest extends AbstractStructuredConcurrencyTest { + @Override + protected Predicate checkTrace() { + // 'parent' with a single 'child' + return trace -> + trace.getSpans().size() == 2 + && findRootSpan(trace, "parent") + .filter(parent -> hasChildSpan(trace, "child", parent.getSpanId())) + .isPresent(); + } +} diff --git a/dd-smoke-tests/src/main/java/datadog/smoketest/AbstractJavaSmokeTest.java b/dd-smoke-tests/src/main/java/datadog/smoketest/AbstractJavaSmokeTest.java new file mode 100644 index 00000000000..d2d4b2ee89c --- /dev/null +++ b/dd-smoke-tests/src/main/java/datadog/smoketest/AbstractJavaSmokeTest.java @@ -0,0 +1,385 @@ +package datadog.smoketest; + +import static datadog.environment.JavaVirtualMachine.isJ9; +import static datadog.trace.api.ProtocolVersion.V0_4; +import static datadog.trace.api.ProtocolVersion.V0_5; +import static datadog.trace.api.ProtocolVersion.V1_0; +import static datadog.trace.test.util.ForkedTestUtils.getMaxMemoryArgumentForFork; +import static datadog.trace.test.util.ForkedTestUtils.getMinMemoryArgumentForFork; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.Duration.ofSeconds; +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.joining; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.environment.OperatingSystem; +import datadog.trace.agent.test.server.http.JavaTestHttpServer; +import datadog.trace.agent.test.server.http.JavaTestHttpServer.HandlerApi.RequestApi; +import datadog.trace.agent.test.server.http.JavaTestHttpServer.HandlerApi.ResponseApi; +import datadog.trace.agent.test.utils.PortUtils; +import datadog.trace.api.ProtocolVersion; +import datadog.trace.test.agent.decoder.DecodedMessage; +import datadog.trace.test.agent.decoder.DecodedTrace; +import datadog.trace.test.agent.decoder.Decoder; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +/** + * Java-only smoke-test base for tests that launch one instrumented process and assert decoded + * traces. + * + *

This temporarily duplicates the relevant behavior from the Groovy smoke-test hierarchy so + * individual smoke tests can migrate to JUnit 5 without requiring a repository-wide migration. + */ +public abstract class AbstractJavaSmokeTest { + private static final int PROFILING_START_DELAY_SECONDS = 1; + private static final int PROFILING_RECORDING_UPLOAD_PERIOD_SECONDS = 5; + private static final int TRACE_WAIT_TIMEOUT_SECONDS = 30; + + private static final String SERVICE_NAME = "smoke-test-java-app"; + private static final String ENV = "smoketest"; + private static final String VERSION = "99"; + private static final String API_KEY = "01234567890abcdef123456789ABCDEF"; + private static final String AGENT_INFO_RESPONSE = + readResource(AbstractJavaSmokeTest.class, "agent-info-response.json"); + + private static final List NOISY_ENVIRONMENT_VARIABLES = + Arrays.asList("CI_COMMIT_TITLE", "CI_COMMIT_MESSAGE", "CI_COMMIT_DESCRIPTION"); + + private static final DateTimeFormatter LOG_FILE_TIMESTAMP_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmmss.SSS", Locale.ROOT).withZone(ZoneOffset.UTC); + + protected final AtomicInteger traceCount = new AtomicInteger(); + + private final CopyOnWriteArrayList decodedTraces = new CopyOnWriteArrayList<>(); + + protected Path buildDirectory; + private String agentShadowJarPath; + protected List defaultJavaProperties; + private OutputThreads outputThreads; + private JavaTestHttpServer server; + protected Process testedProcess; + private Path logFilePath; + private volatile Throwable traceDecodingFailure; + + @BeforeEach + protected void startSmokeTest() throws Exception { + buildDirectory = requiredDirectory("datadog.smoketest.builddir"); + agentShadowJarPath = requiredFile("datadog.smoketest.agent.shadowJar.path").toString(); + traceCount.set(0); + decodedTraces.clear(); + traceDecodingFailure = null; + outputThreads = new OutputThreads(); + server = createServer(); + defaultJavaProperties = createDefaultJavaProperties(); + + String timestamp = LOG_FILE_TIMESTAMP_FORMATTER.format(Instant.now()); + logFilePath = + buildDirectory + .resolve("reports") + .resolve("testProcess." + getClass().getName() + "." + timestamp + ".0.log"); + Files.createDirectories(logFilePath.getParent()); + + ProcessBuilder processBuilder = createProcessBuilder(); + Map environment = processBuilder.environment(); + environment.put("JAVA_HOME", System.getProperty("java.home")); + environment.put("DD_API_KEY", API_KEY); + environment.keySet().removeAll(NOISY_ENVIRONMENT_VARIABLES); + processBuilder.redirectErrorStream(true); + + testedProcess = processBuilder.start(); + outputThreads.captureOutput(testedProcess, logFilePath.toFile()); + if (!testedProcess.isAlive()) { + assertEquals( + 0, testedProcess.exitValue(), "Process exited abnormally; log file: " + logFilePath); + } + } + + @AfterEach + protected void stopSmokeTest() throws Exception { + Throwable failure = null; + failure = runCleanup(failure, this::stopProcess); + if (outputThreads != null) { + failure = runCleanup(failure, outputThreads::close); + } + failure = runCleanup(failure, this::assertNoErrorLogs); + if (server != null) { + failure = runCleanup(failure, server::close); + } + if (traceDecodingFailure != null) { + failure = addFailure(failure, traceDecodingFailure); + } + if (failure != null) { + rethrow(failure); + } + } + + protected abstract ProcessBuilder createProcessBuilder(); + + protected final void waitForTrace(Predicate predicate) { + await("matching trace") + .atMost(ofSeconds(TRACE_WAIT_TIMEOUT_SECONDS)) + .until( + () -> { + throwIfTraceDecodingFailed(); + return decodedTraces.stream().anyMatch(predicate); + }); + } + + private JavaTestHttpServer createServer() { + return JavaTestHttpServer.httpServer( + configuredServer -> + configuredServer.handlers( + handlers -> { + handlers.prefix( + "/info", api -> api.getResponse().status(200).send(AGENT_INFO_RESPONSE)); + handlers.prefix( + "/v0.4/traces", + api -> handleTrace(V0_4, api.getRequest(), api.getResponse())); + handlers.prefix( + "/v0.5/traces", + api -> handleTrace(V0_5, api.getRequest(), api.getResponse())); + handlers.prefix( + "/v1.0/traces", + api -> handleTrace(V1_0, api.getRequest(), api.getResponse())); + handlers.prefix("/v0.6/stats", api -> api.getResponse().status(200).send()); + handlers.prefix("/v0.7/config", api -> api.getResponse().status(200).send("{}")); + handlers.prefix( + "/telemetry/proxy/api/v2/apmtelemetry", + api -> api.getResponse().status(202).send()); + handlers.prefix("/evp_proxy/v2/", api -> api.getResponse().status(200).send()); + })); + } + + private void handleTrace(ProtocolVersion protocol, RequestApi request, ResponseApi response) { + String countHeader = request.getHeader("X-Datadog-Trace-Count"); + int expectedTraceCount = countHeader == null ? 0 : Integer.parseInt(countHeader); + byte[] body = request.getBody(); + if (body.length > 0) { + try { + DecodedMessage message; + switch (protocol) { + case V0_4: + message = Decoder.decodeV04(body); + break; + case V0_5: + message = Decoder.decodeV05(body); + break; + case V1_0: + message = Decoder.decodeV1(body); + break; + default: + throw new IllegalArgumentException("Unsupported trace protocol: " + protocol); + } + assertEquals(expectedTraceCount, message.getTraces().size()); + decodedTraces.addAll(message.getTraces()); + } catch (RuntimeException | Error failure) { + traceDecodingFailure = failure; + throw failure; + } + } + traceCount.addAndGet(expectedTraceCount); + System.out.println("Received " + protocol + " traces: " + countHeader); + response.status(200).send(); + } + + private List createDefaultJavaProperties() { + String temporaryDirectory = "/tmp"; + String preferencesDirectory = + temporaryDirectory + "/userPrefs/" + getClass().getSimpleName() + "_" + System.nanoTime(); + boolean isIbmJvm = System.getProperty("java.vendor", "").contains("IBM"); + boolean isDdprofSafe = !isJ9(); + + List properties = new ArrayList<>(); + properties.add(getMaxMemoryArgumentForFork()); + properties.add(getMinMemoryArgumentForFork()); + properties.add("-javaagent:" + agentShadowJarPath); + properties.add( + isIbmJvm + ? "-Xdump:directory=" + temporaryDirectory + : "-XX:ErrorFile=" + temporaryDirectory + "/hs_err_pid%p.log"); + properties.add("-Ddd.trace.agent.port=" + server.getAddress().getPort()); + properties.add("-Ddd.env=" + ENV); + properties.add("-Ddd.version=" + VERSION); + properties.add("-Ddd.profiling.enabled=true"); + properties.add("-Ddd.profiling.start-delay=" + PROFILING_START_DELAY_SECONDS); + properties.add("-Ddd.profiling.upload.period=" + PROFILING_RECORDING_UPLOAD_PERIOD_SECONDS); + properties.add("-Ddd.profiling.url=http://localhost:" + PortUtils.randomOpenPort() + "/"); + properties.add("-Ddd.profiling.ddprof.enabled=" + isDdprofSafe); + properties.add("-Ddd.profiling.ddprof.alloc.enabled=" + isDdprofSafe); + properties.add("-Ddatadog.slf4j.simpleLogger.defaultLogLevel=info"); + properties.add("-Dorg.slf4j.simpleLogger.defaultLogLevel=info"); + properties.add("-Ddd.site="); + properties.add("-Djava.util.prefs.userRoot=" + preferencesDirectory); + properties.add("-Ddd.service.name=" + SERVICE_NAME); + properties.add("-Ddd.telemetry.heartbeat.interval=5"); + if (!isJ9()) { + properties.add( + "-XX:OnError=" + + temporaryDirectory + + "/dd_crash_uploader." + + getScriptExtension() + + " %p"); + } + + // Disable CDS to avoid SIGSEGVs on Linux arm64. + if (OperatingSystem.isLinux() && OperatingSystem.architecture().isArm64()) { + properties.add("-Xshare:off"); + } + return properties; + } + + private void stopProcess() throws InterruptedException { + if (testedProcess == null) { + return; + } + + if (testedProcess.isAlive()) { + System.err.println("Destroying instrumented process"); + testedProcess.destroy(); + if (!testedProcess.waitFor(5, SECONDS)) { + System.err.println("Destroying instrumented process (forced)"); + testedProcess.destroyForcibly(); + if (!testedProcess.waitFor(10, SECONDS)) { + throw new IllegalStateException("Instrumented process failed to exit after SIGKILL"); + } + } + } + System.err.println("Instrumented process exited with " + testedProcess.exitValue()); + } + + private void assertNoErrorLogs() throws IOException { + if (logFilePath == null || !Files.exists(logFilePath)) { + return; + } + + List errorLogs = new ArrayList<>(); + try (BufferedReader reader = Files.newBufferedReader(logFilePath)) { + String line; + while ((line = reader.readLine()) != null) { + if (isErrorLog(line)) { + errorLogs.add(line); + } + } + } + + StringBuilder message = new StringBuilder("Test application log contains errors:\n"); + for (int index = 0; index < errorLogs.size(); index++) { + message.append(index + 1).append(": ").append(errorLogs.get(index)).append('\n'); + } + assertTrue(errorLogs.isEmpty(), message.toString()); + } + + private static boolean isErrorLog(String line) { + if (line.contains( + "ERROR com.datadog.profiling.controller.ProfilingSystem - Fatal exception in profiling thread, trying to continue")) { + return false; + } + if (line.contains("I/O reactor terminated abnormally")) { + return false; + } + if (line.contains( + "ERROR com.datadog.profiling.controller.ProfilingSystem - Fatal exception during profiling startup")) { + return false; + } + if (line.contains( + "ERROR datadog.trace.agent.jmxfetch.JMXFetch - jmx collector exited with result: 0")) { + return false; + } + return line.contains("ERROR") + || line.contains("ASSERTION FAILED") + || line.contains("Failed to handle exception in instrumentation"); + } + + protected static String readResource(Class resourceClass, String name) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(resourceClass.getResourceAsStream(name), UTF_8))) { + return reader.lines().collect(joining("\n")); + } catch (Throwable e) { + throw new IllegalStateException("Failed to read resource: " + name, e); + } + } + + private void throwIfTraceDecodingFailed() throws Exception { + if (traceDecodingFailure != null) { + rethrow(traceDecodingFailure); + } + } + + private static Path requiredDirectory(String propertyName) { + Path path = requiredPath(propertyName); + assertTrue(Files.isDirectory(path), propertyName + " is not a directory: " + path); + return path; + } + + private static Path requiredFile(String propertyName) { + Path path = requiredPath(propertyName); + assertTrue(Files.isRegularFile(path), propertyName + " is not a file: " + path); + return path; + } + + private static Path requiredPath(String propertyName) { + String value = System.getProperty(propertyName); + assertTrue( + value != null && !value.isEmpty(), + "Expected system property " + propertyName + ". Smoke tests must run from Gradle."); + return Paths.get(value); + } + + private static String getScriptExtension() { + return OperatingSystem.isWindows() ? "bat" : "sh"; + } + + private static Throwable runCleanup(Throwable currentFailure, CleanupAction action) { + try { + action.run(); + return currentFailure; + } catch (Throwable failure) { + return addFailure(currentFailure, failure); + } + } + + private static Throwable addFailure(Throwable currentFailure, Throwable nextFailure) { + if (currentFailure == null) { + return nextFailure; + } + if (currentFailure != nextFailure) { + currentFailure.addSuppressed(nextFailure); + } + return currentFailure; + } + + private static void rethrow(Throwable failure) throws Exception { + if (failure instanceof Error) { + throw (Error) failure; + } + if (failure instanceof Exception) { + throw (Exception) failure; + } + throw new IllegalStateException(failure); + } + + @FunctionalInterface + private interface CleanupAction { + void run() throws Exception; + } +} diff --git a/dd-smoke-tests/src/main/resources/datadog/smoketest/agent-info-response.json b/dd-smoke-tests/src/main/resources/datadog/smoketest/agent-info-response.json new file mode 100644 index 00000000000..fa661a898aa --- /dev/null +++ b/dd-smoke-tests/src/main/resources/datadog/smoketest/agent-info-response.json @@ -0,0 +1,15 @@ +{ + "version": "7.54.1", + "git_commit": "44d1992", + "endpoints": [ + "/v0.4/traces", + "/v0.5/traces", + "/v1.0/traces", + "/telemetry/proxy/", + "/evp_proxy/v2/" + ], + "client_drop_p0s": true, + "span_meta_structs": true, + "long_running_spans": true, + "config": {} +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f9412b2042..a4e502d3a40 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,12 +69,13 @@ moshi = "1.11.0" # Testing assertj = "3.27.7" +awaitility = "4.3.0" +json-unit = "2.40.1" junit4 = "4.13.2" junit5 = "5.14.1" junit-platform = "1.14.1" mockito = "4.4.0" spock = "2.4-groovy-3.0" -json-unit = "2.40.1" tabletest = "1.2.1" testcontainers = "1.21.4" @@ -158,6 +159,7 @@ moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } # Testing assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } +awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" } json-unit-assertj = { module = "net.javacrumbs.json-unit:json-unit-assertj", version.ref = "json-unit" } junit4 = { module = "junit:junit", version.ref = "junit4" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" } diff --git a/products/feature-flagging/feature-flagging-api/build.gradle.kts b/products/feature-flagging/feature-flagging-api/build.gradle.kts index def6a16da8c..f2cbe88522b 100644 --- a/products/feature-flagging/feature-flagging-api/build.gradle.kts +++ b/products/feature-flagging/feature-flagging-api/build.gradle.kts @@ -51,7 +51,7 @@ dependencies { testImplementation(libs.bundles.junit5) testImplementation(libs.bundles.mockito) testImplementation(libs.moshi) - testImplementation("org.awaitility:awaitility:4.3.0") + testImplementation(libs.awaitility) } fun AbstractCompile.configureCompiler(