diff --git a/dd-java-agent/agent-ci-visibility/civisibility-test-fixtures/src/main/groovy/datadog/trace/civisibility/CiVisibilityTestUtils.groovy b/dd-java-agent/agent-ci-visibility/civisibility-test-fixtures/src/main/groovy/datadog/trace/civisibility/CiVisibilityTestUtils.groovy index e433a69efc8..b19e9583df9 100644 --- a/dd-java-agent/agent-ci-visibility/civisibility-test-fixtures/src/main/groovy/datadog/trace/civisibility/CiVisibilityTestUtils.groovy +++ b/dd-java-agent/agent-ci-visibility/civisibility-test-fixtures/src/main/groovy/datadog/trace/civisibility/CiVisibilityTestUtils.groovy @@ -16,6 +16,7 @@ import freemarker.core.InvalidReferenceException import freemarker.template.Template import freemarker.template.TemplateException import freemarker.template.TemplateExceptionHandler +import groovy.transform.CompileStatic import org.opentest4j.AssertionFailedError import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode @@ -367,6 +368,7 @@ abstract class CiVisibilityTestUtils { return compiledPaths } + @CompileStatic // Workaround for Groovy 4 to not produce false-positive FindBugs warning `UPM_UNCALLED_PRIVATE_METHOD`. private static DynamicPath path(String rawPath, boolean unique = true) { return new DynamicPath(rawPath, JsonPath.compile(rawPath), unique) } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/HashCodeTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/HashCodeTest.groovy index 86e300108e4..668a1c91806 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/HashCodeTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/HashCodeTest.groovy @@ -1,6 +1,7 @@ package com.datadog.iast.taint import datadog.trace.test.util.DDSpecification +import groovy.transform.CompileStatic import spock.lang.IgnoreIf import spock.lang.Shared @@ -51,6 +52,7 @@ class HashCodeTest extends DDSpecification { } } + @CompileStatic // Workaround to avoid NPE in Groovy 4 `org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache()` method. private static List genHashCodes(final int n) { (1..n).collect { System.identityHashCode(Double.toString(Math.random())) diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/ObjectGen.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/ObjectGen.groovy index a08038be898..99e85e66735 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/ObjectGen.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/ObjectGen.groovy @@ -2,9 +2,12 @@ package com.datadog.iast.taint import static TaintedMap.POSITIVE_MASK +import groovy.transform.CompileStatic + /** * Generate objects to test {@link TaintedMap}. */ +@CompileStatic // Workaround to avoid java.lang.AbstractMethodError with Groovy 4. class ObjectGen { final int capacity @@ -36,7 +39,8 @@ class ObjectGen { } } - def genBucket(int nObjects, Closure isValid) { + // Have to return specific type to avoid java.lang.AbstractMethodError with Groovy 4. + List genBucket(int nObjects, Closure isValid) { assert nObjects > 0 while (true) { for (int i = 0; i < capacity; i++) { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy index 2483d5af410..bab3c2452b0 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy @@ -793,8 +793,8 @@ class WAFModuleSpecification extends DDSpecification { stackTrace.language == 'java' stackTrace.message == 'Exploit detected' stackTrace.frames.size() >= 1 - stackTrace.frames[0].class_name == 'org.codehaus.groovy.runtime.callsite.CallSiteArray' - stackTrace.frames[0].function == 'defaultCall' + stackTrace.frames[0].class_name == 'org.codehaus.groovy.vmplugin.v8.IndyInterface' // With Groovy 3 it was 'org.codehaus.groovy.runtime.callsite.CallSiteArray' + stackTrace.frames[0].function == 'fromCache' // With Groovy 3 it was 'defaultCall' } void 'redaction with default settings'() { diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/CompletableFutureTest.groovy b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/CompletableFutureTest.groovy index 022ac7e169a..cd4626964a7 100644 --- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/CompletableFutureTest.groovy +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/CompletableFutureTest.groovy @@ -127,7 +127,7 @@ class CompletableFutureTest extends InstrumentationSpecification { // The parent and the child spans can finish out of order since they run // on different threads concurrently trace(2) { - def pIndex = span(0).isRootSpan() ? 0 : 1 + def pIndex = span(0).checkRootSpan() ? 0 : 1 def cIndex = 1 - pIndex basicSpan(it, pIndex, "parent") basicSpan(it, cIndex, "child", span(pIndex)) diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/CrossedContextTest.groovy b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/CrossedContextTest.groovy index 66ee256edb4..323bb2f5c53 100644 --- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/CrossedContextTest.groovy +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/CrossedContextTest.groovy @@ -60,7 +60,7 @@ class CrossedContextTest extends InstrumentationSpecification { then: for (List trace : TEST_WRITER) { assert trace.size() == 2 - DDSpan parent = trace.find({ it.isRootSpan() }) + DDSpan parent = trace.find({ it.checkRootSpan() }) assert null != parent DDSpan child = trace.find({ it.getParentId() == parent.getSpanId() }) assert null != child @@ -121,7 +121,7 @@ class CrossedContextTest extends InstrumentationSpecification { then: for (List trace : TEST_WRITER) { assert trace.size() == 2 - DDSpan parent = trace.find({ it.isRootSpan() }) + DDSpan parent = trace.find({ it.checkRootSpan() }) assert null != parent DDSpan child = trace.find({ it.getParentId() == parent.getSpanId() }) assert null != child diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/executor/RecursiveThreadPoolPropagationTest.groovy b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/executor/RecursiveThreadPoolPropagationTest.groovy index 957bf8cb1da..e50a9fb5209 100644 --- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/executor/RecursiveThreadPoolPropagationTest.groovy +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/groovy/executor/RecursiveThreadPoolPropagationTest.groovy @@ -346,7 +346,7 @@ class RecursiveThreadPoolPropagationTest extends InstrumentationSpecification { List trace = TEST_WRITER.get(0) assert trace.size() == depth for (DDSpan span : sortByDepth(trace)) { - orphanCount += span.isRootSpan() ? 1 : 0 + orphanCount += span.checkRootSpan() ? 1 : 0 assert String.valueOf(i++) == span.getOperationName() } assert orphanCount == 1 diff --git a/dd-java-agent/instrumentation/java/java-nio-1.8/src/test/groovy/DirectAllocationTrackingTest.groovy b/dd-java-agent/instrumentation/java/java-nio-1.8/src/test/groovy/DirectAllocationTrackingTest.groovy index f73585a46e6..1583d6c9555 100644 --- a/dd-java-agent/instrumentation/java/java-nio-1.8/src/test/groovy/DirectAllocationTrackingTest.groovy +++ b/dd-java-agent/instrumentation/java/java-nio-1.8/src/test/groovy/DirectAllocationTrackingTest.groovy @@ -53,12 +53,12 @@ class DirectAllocationTrackingTest extends InstrumentationSpecification { def sample = directAllocations.find({ it.getEventType().name.equals("datadog.DirectAllocationSample")}) sample.getLong("allocated") == 20 sample.getString("source") == "MMAP" - sample.getString("allocatingClass") == "org.codehaus.groovy.runtime.callsite.PlainObjectMetaMethodSite" + sample.getString("allocatingClass") == "org.codehaus.groovy.vmplugin.v8.IndyInterface" // With Groovy 3 it was "org.codehaus.groovy.runtime.callsite.PlainObjectMetaMethodSite" sample.getLong("spanId") == expectedSpanId.get() def total = directAllocations.find({ it.getEventType().name.equals("datadog.DirectAllocationTotal")}) total.getLong("allocated") == 20 total.getString("source") == "MMAP" - total.getString("allocatingClass") == "org.codehaus.groovy.runtime.callsite.PlainObjectMetaMethodSite" + total.getString("allocatingClass") == "org.codehaus.groovy.vmplugin.v8.IndyInterface" // With Groovy 3 it was "org.codehaus.groovy.runtime.callsite.PlainObjectMetaMethodSite" cleanup: recording.close() @@ -80,12 +80,12 @@ class DirectAllocationTrackingTest extends InstrumentationSpecification { def sample = directAllocations.find({ it.getEventType().name.equals("datadog.DirectAllocationSample")}) sample.getLong("allocated") == 10 sample.getString("source") == "ALLOCATE_DIRECT" - sample.getString("allocatingClass") == "java_nio_ByteBuffer\$allocateDirect" + sample.getString("allocatingClass") == "org.codehaus.groovy.vmplugin.v8.IndyInterface" // With Groovy 3 it was "java_nio_ByteBuffer\$allocateDirect" sample.getLong("spanId") == expectedSpanId.get() def total = directAllocations.find({ it.getEventType().name.equals("datadog.DirectAllocationTotal")}) total.getLong("allocated") == 10 total.getString("source") == "ALLOCATE_DIRECT" - total.getString("allocatingClass") == "java_nio_ByteBuffer\$allocateDirect" + total.getString("allocatingClass") == "org.codehaus.groovy.vmplugin.v8.IndyInterface"// With Groovy 3 it was "java_nio_ByteBuffer\$allocateDirect" cleanup: recording.close() diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/build.gradle b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/build.gradle index bb8e09d275f..396f2344251 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/build.gradle +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/build.gradle @@ -32,6 +32,14 @@ dependencies { latestDepTestImplementation group: 'org.spockframework', name: 'spock-core', version: '2.+' } +// Exclude Groovy 4 to test old Spock versions, compatible with Groovy 3. +// TODO: Check if we can either drop old versions or support both Groovy 3 & Groovy 4. +configurations { + testImplementation { + exclude group: 'org.apache.groovy' + } +} + configurations.matching({ it.name.startsWith('test') }).configureEach({ it.resolutionStrategy { force group: 'org.junit.platform', name: 'junit-platform-launcher', version: libs.versions.junit.platform.get() diff --git a/dd-java-agent/instrumentation/liberty/liberty-23.0/build.gradle b/dd-java-agent/instrumentation/liberty/liberty-23.0/build.gradle index f90c9eed237..7422a67fdda 100644 --- a/dd-java-agent/instrumentation/liberty/liberty-23.0/build.gradle +++ b/dd-java-agent/instrumentation/liberty/liberty-23.0/build.gradle @@ -65,7 +65,6 @@ dependencies { configurations.named("testRuntimeOnly") { exclude group: 'ch.qos.logback', module: 'logback-classic' - exclude group: 'org.codehaus.groovy', module: 'groovy-servlet' } configurations.named("webappRuntimeClasspath") { exclude group: 'ch.qos.logback', module: 'logback-classic' diff --git a/dd-java-agent/instrumentation/ratpack-1.5/build.gradle b/dd-java-agent/instrumentation/ratpack-1.5/build.gradle index e6572cbe135..719ca7ebe35 100644 --- a/dd-java-agent/instrumentation/ratpack-1.5/build.gradle +++ b/dd-java-agent/instrumentation/ratpack-1.5/build.gradle @@ -17,8 +17,12 @@ dependencies { compileOnly group: 'io.ratpack', name: 'ratpack-core', version: '1.5.0' testImplementation project(':dd-java-agent:instrumentation:netty:netty-4.1') - testImplementation group: 'io.ratpack', name: 'ratpack-groovy-test', version: '1.5.0' + testImplementation(group: 'io.ratpack', name: 'ratpack-groovy-test', version: '1.5.0') { + exclude group: 'org.codehaus.groovy' + } testImplementation 'com.sun.activation:jakarta.activation:1.2.2' testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures') - latestDepTestImplementation group: 'io.ratpack', name: 'ratpack-groovy-test', version: '1.+' + latestDepTestImplementation(group: 'io.ratpack', name: 'ratpack-groovy-test', version: '1.+') { + exclude group: 'org.codehaus.groovy' + } } diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy index 30ee20fb1ef..728a03b1294 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy @@ -6,6 +6,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.core.DDSpan import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import groovy.transform.CompileStatic import org.eclipse.jetty.http.HttpMethod import org.eclipse.jetty.http.HttpVersion import org.eclipse.jetty.server.Handler @@ -331,6 +332,7 @@ class TestHttpServer implements AutoCloseable { this.prefix = prefix.startsWith("/") ? prefix : "/" + prefix } + @CompileStatic // Groovy 4: workaround to avoid `groovy.lang.MissingMethodException`. @Override void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (target.startsWith(prefix)) { @@ -503,7 +505,7 @@ class TestHttpServer implements AutoCloseable { } void send(byte[] body) { - sendWithType(DEFAULT_TYPE, body) + sendWithType(DEFAULT_TYPE, (byte[]) body) // Groovy 4 has stricter coercions rules. } void sendWithType(String contentType, byte[] body) { diff --git a/dd-smoke-tests/gradle/build.gradle b/dd-smoke-tests/gradle/build.gradle index 76ebc34b6cb..f02c99fce08 100644 --- a/dd-smoke-tests/gradle/build.gradle +++ b/dd-smoke-tests/gradle/build.gradle @@ -8,6 +8,18 @@ plugins { apply from: "$rootDir/gradle/java.gradle" description = 'Gradle Daemon Instrumentation Smoke Tests.' +// Gradle 8.14.3 bundles Groovy 3 via `gradleTestKit()`, so we need Spock for Groovy 3 instead of Groovy 4. +// Gradle 9.x bundles Groovy 4, so eventually we can drop this workaround. +configurations.configureEach { + // Exclude Groovy 4. Starting from v4 it has group: `org.apache.groovy`. + exclude group: 'org.apache.groovy' + + resolutionStrategy { + // Force Spock for Groovy 3. + force "org.spockframework:spock-core:2.4-groovy-3.0" + } +} + dependencies { testImplementation gradleTestKit() testImplementation project(':dd-smoke-tests:backend-mock') diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index f434fe820b9..4106495e996 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -1613,7 +1613,7 @@ protected static final DDSpan buildSpanImpl( List links, DDSpanContext spanContext) { DDSpan span = DDSpan.create(instrumentationName, timestampMicro, spanContext, links); - if (span.isLocalRootSpan()) { + if (span.checkLocalRootSpan()) { EndpointTracker tracker = tracer.onRootSpanStarted(span); if (tracker != null) { span.setEndpointTracker(tracker); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index 1b321f4536a..3b9ed37e9f5 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -310,7 +310,7 @@ public boolean isForceKeep() { * * @return true if root, false otherwise */ - public final boolean isRootSpan() { + public final boolean checkRootSpan() { return context.getParentId() == DDSpanId.ZERO; } @@ -330,7 +330,7 @@ public DDSpan getLocalRootSpan() { * * @return {@literal true} if this span is the same as {@linkplain #getLocalRootSpan()} */ - public boolean isLocalRootSpan() { + public boolean checkLocalRootSpan() { return getLocalRootSpan().equals(this); } @@ -382,7 +382,7 @@ private boolean isExceptionReplayEnabled() { boolean captureOnlyRootSpan = (Config.get().isDebuggerExceptionOnlyLocalRoot() || !Config.get().isDebuggerExceptionCaptureIntermediateSpansEnabled()); - if (captureOnlyRootSpan && !isLocalRootSpan()) { + if (captureOnlyRootSpan && !checkLocalRootSpan()) { return false; } return true; diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanTest.groovy index e7882432d9c..a61fffbab48 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanTest.groovy @@ -279,14 +279,14 @@ class DDSpanTest extends DDCoreSpecification { new ExtractedContext(DDTraceId.ONE, 2, PrioritySampling.SAMPLER_DROP, "some-origin", propagationTagsFactory.empty(), DATADOG) | _ } - def "isRootSpan() in and not in the context of distributed tracing"() { + def "checkRootSpan() in and not in the context of distributed tracing"() { setup: def root = tracer.buildSpan("root").asChildOf((AgentSpanContext) extractedContext).start() def child = tracer.buildSpan("child").asChildOf(root).start() expect: - root.isRootSpan() == isTraceRootSpan - !child.isRootSpan() + root.checkRootSpan() == isTraceRootSpan + !child.checkRootSpan() cleanup: child.finish() diff --git a/gradle/codenarc.gradle b/gradle/codenarc.gradle index 563d181a0fe..459f8c2c51f 100644 --- a/gradle/codenarc.gradle +++ b/gradle/codenarc.gradle @@ -1,7 +1,7 @@ apply plugin: "codenarc" dependencies { - codenarc('org.codenarc:CodeNarc:3.7.0') + codenarc('org.codenarc:CodeNarc:3.7.0-groovy-4.0') } codenarc { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index de4d63c8ab5..69d803ec3d3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ okhttp = "3.12.15" # Datadog fork to support Java 7 # Languages ## Groovy -groovy = "3.0.25" +groovy = "4.0.29" ## Kotlin kotlin = "1.6.21" @@ -67,7 +67,7 @@ junit4 = "4.13.2" junit5 = "5.14.1" junit-platform = "1.14.1" mockito = "4.4.0" -spock = "2.4-groovy-3.0" +spock = "2.4-groovy-4.0" testcontainers = "1.21.3" [libraries] @@ -83,11 +83,11 @@ okhttp = { module = "com.datadoghq.okhttp3:okhttp", version.ref = "okhttp" } # Languages ## Groovy -groovy = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" } -groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" } -groovy-templates = { module = "org.codehaus.groovy:groovy-templates", version.ref = "groovy" } -groovy-yaml = { module = "org.codehaus.groovy:groovy-yaml", version.ref = "groovy" } -groovy-xml = { module = "org.codehaus.groovy:groovy-xml", version.ref = "groovy" } +groovy = { module = "org.apache.groovy:groovy", version.ref = "groovy" } +groovy-json = { module = "org.apache.groovy:groovy-json", version.ref = "groovy" } +groovy-templates = { module = "org.apache.groovy:groovy-templates", version.ref = "groovy" } +groovy-yaml = { module = "org.apache.groovy:groovy-yaml", version.ref = "groovy" } +groovy-xml = { module = "org.apache.groovy:groovy-xml", version.ref = "groovy" } ## Kotlin kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } diff --git a/remote-config/remote-config-core/src/test/groovy/datadog/remoteconfig/DefaultConfigurationPollerSpecification.groovy b/remote-config/remote-config-core/src/test/groovy/datadog/remoteconfig/DefaultConfigurationPollerSpecification.groovy index ae18bd5f839..170409ba388 100644 --- a/remote-config/remote-config-core/src/test/groovy/datadog/remoteconfig/DefaultConfigurationPollerSpecification.groovy +++ b/remote-config/remote-config-core/src/test/groovy/datadog/remoteconfig/DefaultConfigurationPollerSpecification.groovy @@ -588,7 +588,7 @@ class DefaultConfigurationPollerSpecification extends DDSpecification { byte[] fileDecoded = Base64.decoder.decode(it['target_files'][0]['raw']) byte[] newFile = new byte[fileDecoded.length + 1] System.arraycopy(fileDecoded, 0, newFile, 0, fileDecoded.length) - newFile[fileDecoded.length] = '\n' + newFile[fileDecoded.length] = (byte) '\n' // Groovy 4 has strict coercion rules. it['target_files'][0]['raw'] = Base64.encoder.encodeToString(newFile) def targetDecoded = Base64.decoder.decode(it['targets']) def target = SLURPER.parse(targetDecoded)