diff --git a/dd-smoke-tests/concurrent/java-25/build.gradle b/dd-smoke-tests/concurrent/java-25/build.gradle index 8aeb5484287..f971b3f1ada 100644 --- a/dd-smoke-tests/concurrent/java-25/build.gradle +++ b/dd-smoke-tests/concurrent/java-25/build.gradle @@ -7,14 +7,11 @@ plugins { } 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 + // Newer JVMs can run ordinary Java 25 bytecode, but these are tests for preview of StructuredTaskScope API. + // Preview bytecode runs only on the Java release that compiled it. + // Even with --enable-preview, Java 27 accepts only Java 27 preview bytecode, not Java 25's. + maxJavaVersion = JavaVersion.VERSION_25 } description = 'JDK 25 Concurrent Integration Tests' @@ -32,13 +29,6 @@ tasks.named('compileJava', JavaCompile) { options.compilerArgs.add("--enable-preview") } -// TODO: drop Groovy here at all -// The Groovy test framework can't run on Java 25 (the module's default toolchain), so pin the -// Groovy test compiler back to JDK 8 — matches the Java 8 test JVM that forks a Java 25 subprocess. -tasks.named('compileTestGroovy') { - configureCompiler(it, 8, JavaVersion.VERSION_1_8, "Groovy test framework cannot run on JDK 25") -} - application { mainClass = 'datadog.smoketest.concurrent.ConcurrentApp' } @@ -51,7 +41,6 @@ dependencies { tasks.withType(Test).configureEach { def shadowJarTask = tasks.named('shadowJar', ShadowJar) dependsOn shadowJarTask - environment("JAVA_HOME", getLazyJavaHomeFor(25)) jvmArgumentProviders.add(new CommandLineArgumentProvider() { @Override Iterable asArguments() { 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 52ae573544b..00000000000 --- a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/AbstractStructuredConcurrencyTest.groovy +++ /dev/null @@ -1,59 +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 { - @Override - protected boolean expectsProcessAliveBeforeEachTest() { - false - } - - 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/ScheduledForkJoinTaskTest.groovy b/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/ScheduledForkJoinTaskTest.groovy deleted file mode 100644 index 91570bcf747..00000000000 --- a/dd-smoke-tests/concurrent/java-25/src/test/groovy/datadog/smoketest/concurrent/ScheduledForkJoinTaskTest.groovy +++ /dev/null @@ -1,37 +0,0 @@ -package datadog.smoketest.concurrent - -import datadog.trace.test.agent.decoder.DecodedTrace - -import java.util.function.Function - -import static java.util.concurrent.TimeUnit.SECONDS - -class ScheduledForkJoinTaskTest extends AbstractStructuredConcurrencyTest { - @Override - protected String testCaseName() { - 'ScheduledForkJoinTask' - } - - @Override - protected Function checkTrace() { - return { trace -> - def parent = findRootSpan(trace, 'parent') - def scheduled = parent ? findChildSpan(trace, 'scheduled', parent.spanId) : null - return trace.spans.size() == 3 && scheduled && findChildSpan(trace, 'nested', scheduled.spanId) - } - } - - def 'propagates delayed context across executors and clears it after execution'() { - expect: - testedProcess.waitFor(TIMEOUT_SECS, SECONDS) - testedProcess.exitValue() == 0 - waitForTrace(defaultPoll, checkTrace()) - waitForTrace(defaultPoll, { trace -> - trace.spans.size() == 1 && findRootSpan(trace, 'unrelated-forkjoin') - }) - waitForTrace(defaultPoll, { trace -> - trace.spans.size() == 1 && findRootSpan(trace, 'unrelated-executor') - }) - traceCount.get() == 3 - } -} 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..0498d982c0e --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/AbstractStructuredConcurrencyTest.java @@ -0,0 +1,58 @@ +package datadog.smoketest.concurrent; + +import static datadog.smoketest.backend.AgentBackend.mockAgent; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.smoketest.SmokeCliApp; +import datadog.trace.test.agent.decoder.DecodedSpan; +import datadog.trace.test.agent.decoder.DecodedTrace; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class AbstractStructuredConcurrencyTest { + @RegisterExtension + final SmokeCliApp app = + SmokeCliApp.named(testCaseName()) + .jar(System.getProperty("datadog.smoketest.shadowJar.path")) + .jvmArgs("--enable-preview", "-Ddd.trace.otel.enabled=true") + .args(testCaseName()) + .backend(mockAgent()) + .build(); + + protected abstract String testCaseName(); + + protected abstract Predicate checkTrace(); + + protected void receivedCorrectTrace() { + app.assertCompletesWithValue(10, SECONDS, 0); + app.traces().waitForTraceCount(1); + List traces = app.traces().getTraces(); + assertEquals(1, traces.size()); + assertTrue(checkTrace().test(traces.get(0)), "Unexpected structured concurrency trace"); + } + + 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..a76f330ae5b --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/MultipleTasksTest.java @@ -0,0 +1,40 @@ +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..e060db5e531 --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/NestedTasksTest.java @@ -0,0 +1,45 @@ +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/ScheduledForkJoinTaskTest.java b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/ScheduledForkJoinTaskTest.java new file mode 100644 index 00000000000..9f70adbaa50 --- /dev/null +++ b/dd-smoke-tests/concurrent/java-25/src/test/java/datadog/smoketest/concurrent/ScheduledForkJoinTaskTest.java @@ -0,0 +1,48 @@ +package datadog.smoketest.concurrent; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.test.agent.decoder.DecodedTrace; +import java.util.List; +import java.util.function.Predicate; +import org.junit.jupiter.api.Test; + +class ScheduledForkJoinTaskTest extends AbstractStructuredConcurrencyTest { + @Override + protected String testCaseName() { + return "ScheduledForkJoinTask"; + } + + @Override + protected Predicate checkTrace() { + return trace -> + trace.getSpans().size() == 3 + && findRootSpan(trace, "parent") + .flatMap(parent -> findChildSpan(trace, "scheduled", parent.getSpanId())) + .filter(scheduled -> hasChildSpan(trace, "nested", scheduled.getSpanId())) + .isPresent(); + } + + @Test + void propagatesDelayedContextAcrossExecutorsAndClearsItAfterExecution() { + app.assertCompletesWithValue(10, SECONDS, 0); + app.traces().waitForTraceCount(3); + List traces = app.traces().getTraces(); + assertEquals(3, traces.size()); + assertTrue(traces.stream().anyMatch(checkTrace()), "Missing scheduled task trace"); + assertTrue( + traces.stream() + .anyMatch(trace -> + trace.getSpans().size() == 1 + && findRootSpan(trace, "unrelated-forkjoin").isPresent()), + "Missing unrelated fork-join trace"); + assertTrue( + traces.stream() + .anyMatch(trace -> + trace.getSpans().size() == 1 + && findRootSpan(trace, "unrelated-executor").isPresent()), + "Missing unrelated executor trace"); + } +} 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(); + } +}