From 0e4743c45487c14d95d93b736f054039cc355c9d Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 6 Nov 2026 09:40:20 +0100 Subject: [PATCH 1/2] Fix test tasks that pass without running any tests Run every Test task on the JUnit Platform, not only test suites: tasks registered with tasks.register(..., Test) fell back to JUnit 4 since the Gradle 8.1 upgrade and discovered none of the Spock 2 or JUnit 5 tests. Fail a test task that discovers no tests again, except for the non-forked peer of a suite whose concrete tests are all *ForkedTest classes, and for three test source sets that only hold shared abstract bases. Skip the springboot-graphql detection feature in testRuntimeActivation, which loads AppSec without ever activating it. Co-Authored-By: Claude Opus 5.5 --- .../dd-trace-java.configure-tests.gradle.kts | 25 +++++++++++-------- .../exception-profiling/build.gradle | 4 --- .../play-ws/play-ws-common/build.gradle | 5 ++++ .../instrumentation/restlet-2.2/build.gradle | 2 ++ .../scala-promise-common/build.gradle | 5 ++++ dd-java-agent/testing/build.gradle | 4 --- .../SpringBootGraphqlSmokeTest.groovy | 4 +++ dd-smoke-tests/appsec/springboot/build.gradle | 3 --- gradle/test-suites.gradle | 1 - 9 files changed, 30 insertions(+), 23 deletions(-) diff --git a/buildSrc/src/main/kotlin/dd-trace-java.configure-tests.gradle.kts b/buildSrc/src/main/kotlin/dd-trace-java.configure-tests.gradle.kts index 6aa48794c10..55c961d5e50 100644 --- a/buildSrc/src/main/kotlin/dd-trace-java.configure-tests.gradle.kts +++ b/buildSrc/src/main/kotlin/dd-trace-java.configure-tests.gradle.kts @@ -10,6 +10,7 @@ import org.gradle.kotlin.dsl.withType import org.gradle.testing.base.TestingExtension import java.time.Duration import java.time.temporal.ChronoUnit +import java.util.concurrent.Callable plugins { id("dd-trace-java.modifiable-config") @@ -41,6 +42,10 @@ val runFlakyTestsProvider = rootProject.providers.gradleProperty("runFlakyTests" tasks.withType().configureEach { configureTestAgentPostChecks() + // Test suites get the JUnit Platform from `useJUnitJupiter()` above, but tasks registered with + // `tasks.register(..., Test)` would default to JUnit 4 and discover no Spock 2 or JUnit 5 tests. + useJUnitPlatform() + // Disable all tests if skipTests property was specified onlyIf("skipTests are undefined or false") { !skipTestsProvider.isPresent } @@ -67,17 +72,15 @@ tasks.withType().configureEach { onlyIf("skipForkedTests are undefined or false") { !skipForkedTestsProvider.isPresent } } else { exclude("**/*ForkedTest*") - // Starting from Gradle 9.3, Gradle will fail if a test task has no discovered tests. - // While this can be a misconfiguration, dd-trace-java test suite conventions allow suites - // to contain abstract base classes that are extended by concrete forked test suites. - // - // Many instrumentation suites indeed only ship *ForkedTest concrete classes - // (the non-forked peer task created by `addTestSuiteExtendingForDir` exists only - // to host shared configurations/sources). Those test tasks won't have executable tests - // by design under the `**/*ForkedTest*` exclude rule above. So let's allow them. - // - // Related but not the issue here https://github.com/gradle/gradle/issues/36508 - failOnNoDiscoveredTests = false + // Gradle fails a test task that discovers no tests, which catches misconfigured tasks. + // The exception is a suite whose concrete tests are all *ForkedTest classes: the exclude rule + // above leaves its non-forked task with abstract base classes only, so allow it to be empty. + // The compiled classes are read lazily, once the task inputs are snapshotted. + val forkedTestClasses = objects.fileCollection() + .from(Callable { testClassesDirs }) + .asFileTree + .matching { include("**/*ForkedTest*.class") } + failOnNoDiscoveredTests.set(forkedTestClasses.elements.map { it.isEmpty() }) } // Set test timeout for 20 minutes. Default job timeout is 1h (configured on CI level). diff --git a/dd-java-agent/instrumentation/datadog/profiling/exception-profiling/build.gradle b/dd-java-agent/instrumentation/datadog/profiling/exception-profiling/build.gradle index ff446035c39..772c9718c78 100644 --- a/dd-java-agent/instrumentation/datadog/profiling/exception-profiling/build.gradle +++ b/dd-java-agent/instrumentation/datadog/profiling/exception-profiling/build.gradle @@ -34,7 +34,3 @@ dependencies { it.options.compilerArgs.addAll(['-Xlint:all,-processing,-options,-path']) } } - -tasks.named("test", Test) { - useJUnitPlatform() -} diff --git a/dd-java-agent/instrumentation/play-ws/play-ws-common/build.gradle b/dd-java-agent/instrumentation/play-ws/play-ws-common/build.gradle index 1e54d757fb1..4be37577fcf 100644 --- a/dd-java-agent/instrumentation/play-ws/play-ws-common/build.gradle +++ b/dd-java-agent/instrumentation/play-ws/play-ws-common/build.gradle @@ -13,6 +13,11 @@ artifacts { testArtifacts testJar } +tasks.named('test', Test) { + // The test source set only holds the abstract base class reused by the play-ws modules + failOnNoDiscoveredTests = false +} + dependencies { compileOnly group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '1.0.2' diff --git a/dd-java-agent/instrumentation/restlet-2.2/build.gradle b/dd-java-agent/instrumentation/restlet-2.2/build.gradle index 5a526257b83..68a285c2c6e 100644 --- a/dd-java-agent/instrumentation/restlet-2.2/build.gradle +++ b/dd-java-agent/instrumentation/restlet-2.2/build.gradle @@ -27,6 +27,8 @@ addTestSuite('latestDepTest') tasks.named("test", Test) { dependsOn "baseForkedTest" + // The test source set only holds the abstract base and resources shared with baseForkedTest and latestDepTest + failOnNoDiscoveredTests = false } dependencies { diff --git a/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-common/build.gradle b/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-common/build.gradle index 14da6590439..8b0f5c57366 100644 --- a/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-common/build.gradle +++ b/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-common/build.gradle @@ -19,6 +19,11 @@ tasks.named("compileTestGroovy", GroovyCompile) { classpath += files(sourceSets.test.scala.classesDirectory) } +tasks.named("test", Test) { + // The test source set only holds the abstract base classes compiled by the scala-promise-2.x modules + failOnNoDiscoveredTests = false +} + dependencies { compileOnly group: 'org.scala-lang', name: 'scala-library', version: '2.10.7' // First 2.10 version that accepts target 1.8 testImplementation group: 'org.scala-lang', name: 'scala-library', version: '2.10.7' diff --git a/dd-java-agent/testing/build.gradle b/dd-java-agent/testing/build.gradle index 39cf07db1cf..3c95af6544a 100644 --- a/dd-java-agent/testing/build.gradle +++ b/dd-java-agent/testing/build.gradle @@ -90,7 +90,3 @@ configurations { exclude group: 'org.eclipse.jetty.http2' } } - -tasks.withType(Test).configureEach { - useJUnitPlatform() -} diff --git a/dd-smoke-tests/appsec/springboot-graphql/src/test/groovy/datadog/smoketest/appsec/springbootgraphql/SpringBootGraphqlSmokeTest.groovy b/dd-smoke-tests/appsec/springboot-graphql/src/test/groovy/datadog/smoketest/appsec/springbootgraphql/SpringBootGraphqlSmokeTest.groovy index 8ecbe99fba1..316cda9bfa2 100644 --- a/dd-smoke-tests/appsec/springboot-graphql/src/test/groovy/datadog/smoketest/appsec/springbootgraphql/SpringBootGraphqlSmokeTest.groovy +++ b/dd-smoke-tests/appsec/springboot-graphql/src/test/groovy/datadog/smoketest/appsec/springbootgraphql/SpringBootGraphqlSmokeTest.groovy @@ -8,6 +8,7 @@ import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import org.apache.commons.io.IOUtils +import spock.lang.IgnoreIf import spock.lang.Shared import java.nio.charset.StandardCharsets @@ -100,6 +101,9 @@ class SpringBootGraphqlSmokeTest extends AbstractAppSecServerSmokeTest { } } + // testRuntimeActivation loads AppSec without ever activating it, so no attack can be detected. + // TODO APMLP-2199: activate AppSec through remote config and run this feature in that mode too. + @IgnoreIf({ System.getProperty('smoke_test.appsec.enabled') == 'inactive' }) def 'test detect #description'() { setup: diff --git a/dd-smoke-tests/appsec/springboot/build.gradle b/dd-smoke-tests/appsec/springboot/build.gradle index cd0b7407d97..4a1f25d821c 100644 --- a/dd-smoke-tests/appsec/springboot/build.gradle +++ b/dd-smoke-tests/appsec/springboot/build.gradle @@ -54,9 +54,6 @@ tasks.withType(Test).configureEach { tasks.register('testRuntimeActivation', Test) { def shadowJarTask = tasks.named('shadowJar', ShadowJar) - // Without this, the task keeps Gradle's default JUnit 4 runner, which discovers none of the - // Spock specs in this module, so the task passes without running anything. - useJUnitPlatform() // Limit to the exposure spec: the other specs in this source set expect AppSec active (e.g. // SpringBootSmokeTest asserts 403 blocking responses), which is incompatible with the inactive // mode this task runs under. diff --git a/gradle/test-suites.gradle b/gradle/test-suites.gradle index a1c81cd8c4f..78697d11660 100644 --- a/gradle/test-suites.gradle +++ b/gradle/test-suites.gradle @@ -98,7 +98,6 @@ ext.addForkedTestTask = { String sourceSetName -> def taskName = sourceSetName == 'test' ? 'forkedTest' : "${sourceSetName}ForkedTest" return tasks.register(taskName, Test) { group = LifecycleBasePlugin.VERIFICATION_GROUP - useJUnitPlatform() testClassesDirs = sourceSets[sourceSetName].output.classesDirs classpath = sourceSets[sourceSetName].runtimeClasspath } From c6071d74bb70e52185cdbb5eb4f2decc5cfa6f2c Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 6 Nov 2026 15:25:40 +0100 Subject: [PATCH 2/2] Remove the copied spring-tomcat7 testRuntimeActivation task The testRuntimeActivation task of spring-tomcat7 was copied from springboot's build file. Its spec is IAST-only and never applies the inactive AppSec flag, so the task reran the test task unchanged. Co-Authored-By: Claude Opus 5.5 --- dd-smoke-tests/appsec/spring-tomcat7/build.gradle | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/dd-smoke-tests/appsec/spring-tomcat7/build.gradle b/dd-smoke-tests/appsec/spring-tomcat7/build.gradle index 986125c5e09..c8081df545b 100644 --- a/dd-smoke-tests/appsec/spring-tomcat7/build.gradle +++ b/dd-smoke-tests/appsec/spring-tomcat7/build.gradle @@ -33,18 +33,3 @@ tasks.withType(Test).configureEach { } }) } - -tasks.register('testRuntimeActivation', Test) { - def shadowJarTask = tasks.named('shadowJar', ShadowJar) - jvmArgs '-Dsmoke_test.appsec.enabled=inactive' - jvmArgumentProviders.add(new CommandLineArgumentProvider() { - @Override - Iterable asArguments() { - return shadowJarTask.map { ["-Ddatadog.smoketest.appsec.springtomcat7.shadowJar.path=${it.archiveFile.get()}"] }.get() - } - }) -} - -tasks.named('check') { - dependsOn(testRuntimeActivation) -}